#!/bin/bash
# 
# This script aims to manage k8s clusters created by 'kubeasz'. (developing)

set -o nounset
set -o errexit
#set -o xtrace

function usage() {
    cat <<EOF
Usage: easzctl COMMAND [args]

Cluster-wide operation:
    checkout		To switch to cluster <clustername> context, or create it if not existed
    destroy		To destroy the current cluster, with '--purge' option to also delete the context
    list		To list all of clusters managed
    setup		To setup a cluster using the current context
    start-aio		To quickly setup an all-in-one cluster for testing (like minikube)

In-cluster operation:
    add-etcd		To add a etcd-node to the etcd cluster
    add-master		To add a kube-master(master node) to the k8s cluster
    add-node		To add a kube-node(work node) to the k8s cluster
    clean-node		To clean a node, whatever role the node plays
    del-etcd		To delete a etcd-node from the etcd cluster
    upgrade		To upgrade the k8s cluster

Extra operation:
    basic-auth   	To enable/disable basic-auth for apiserver

Use "easzctl help <command>" for more information about a given command.
EOF
}

function help-info() {
    case "$1" in
        (add-node)
            echo -e "Usage: easzctl add-node <new_node_ip>\n\nread 'https://github.com/easzlab/kubeasz/blob/master/docs/op/AddNode.md'"
            ;;
        (add-master)
            echo -e "Usage: easzctl add-master <new_master_ip>\n\nread 'https://github.com/easzlab/kubeasz/blob/master/docs/op/AddMaster.md'"
            ;;
        (add-etcd)
            echo -e "Usage: easzctl add-etcd <new_etcd_ip>\n\nread 'https://github.com/easzlab/kubeasz/blob/master/docs/op/op-etcd.md'"
            ;;
        (del-etcd)
            echo -e "Usage: easzctl del-etcd <etcd_ip>\n\nread 'https://github.com/easzlab/kubeasz/blob/master/docs/op/op-etcd.md'"
            ;;
        (clean-node)
            echo -e "Usage: easzctl clean-node <node_ip>\n\nread 'https://github.com/easzlab/kubeasz/blob/master/docs/op/clean_one_node.md'"
            ;;
        (basic-auth)
            echo -e "Usage: easzctl basic-auth <options>\nOption:\t -s enable basic-auth\n\t -S disable basic-auth\n\t -u <user> set username\n\t -p <pass> set password"
            ;;
        (*)
            usage
            return 0
            ;;
    esac
}

function process_cmd() {
    echo -e "[INFO] \033[33m$ACTION\033[0m : $CMD"
    $CMD || { echo -e "[ERROR] \033[31mAction failed\033[0m : $CMD"; return 1; }
    echo -e "[INFO] \033[32mAction successed\033[0m : $CMD"
}

### in-cluster operation functions ##############################

function add-node() {
    # check new node's address regexp
    [[ $1 =~ ^(2(5[0-5]{1}|[0-4][0-9]{1})|[0-1]?[0-9]{1,2})(\.(2(5[0-5]{1}|[0-4][0-9]{1})|[0-1]?[0-9]{1,2})){3}$ ]] || { echo "[ERROR] Invalid ip address!"; return 1; }

    # check if the new node already exsited
    sed -n '/^\[kube-master/,/^\[harbor/p' $BASEPATH/hosts|grep "^$1" && { echo "[ERROR] node $1 already existed!"; return 2; }

    # add a node into 'kube-node' group
    sed -i "/\[kube-node/a $1 NEW_NODE=yes" $BASEPATH/hosts

    # check if playbook runs successfully
    ansible-playbook $BASEPATH/tools/20.addnode.yml -e NODE_TO_ADD=$1 || { sed -i "/$1 NEW_NODE=yes/d" $BASEPATH/hosts; return 2; }

    # save current cluster context if needed
    [ -f "$BASEPATH/.cluster/current_cluster" ] && save_context
    return 0
}

function add-master() {
    # check new master's address regexp
    [[ $1 =~ ^(2(5[0-5]{1}|[0-4][0-9]{1})|[0-1]?[0-9]{1,2})(\.(2(5[0-5]{1}|[0-4][0-9]{1})|[0-1]?[0-9]{1,2})){3}$ ]] || { echo "[ERROR] Invalid ip address!"; return 2; }

    # check if k8s with DPLOY_MODE='multi-master'
    grep '^DEPLOY_MODE=multi-master' $BASEPATH/hosts || { echo "[ERROR] only k8s with DPLOY_MODE='multi-master' can have master node added!"; return 2; }

    # check if the new master already exsited
    sed -n '/^\[kube-master/,/^\[kube-node/p' $BASEPATH/hosts|grep "^$1" && { echo "[ERROR] master $1 already existed!"; return 2; }

    # add a node into 'kube-master' group
    sed -i "/\[kube-master/a $1 NEW_MASTER=yes" $BASEPATH/hosts

    # check if playbook runs successfully
    ansible-playbook $BASEPATH/tools/21.addmaster.yml -e NODE_TO_ADD=$1 || { sed -i "/$1 NEW_MASTER=yes/d" $BASEPATH/hosts; return 2; }

    # save current cluster context if needed
    [ -f "$BASEPATH/.cluster/current_cluster" ] && save_context
    return 0
}

function add-etcd() {
    # check new node's address regexp
    [[ $1 =~ ^(2(5[0-5]{1}|[0-4][0-9]{1})|[0-1]?[0-9]{1,2})(\.(2(5[0-5]{1}|[0-4][0-9]{1})|[0-1]?[0-9]{1,2})){3}$ ]] || { echo "[ERROR] Invalid ip address!"; return 2; }

    # check if the new node already exsited
    sed -n '/^\[etcd/,/^\[kube-master/p' $BASEPATH/hosts|grep "^$1" && { echo "[ERROR] node $1 already existed!"; return 2; }

    # input an unique NODE_NAME of the node in etcd cluster
    echo "Please input an UNIQUE name(string) for the new node: "
    read -t15 NAME
    sed -n '/^\[etcd/,/^\[kube-master/p' $BASEPATH/hosts|grep "$NAME" && { echo "[ERROR] name [$NAME] already existed!"; return 2; }

    # add a node into 'kube-node' group
    sed -i "/\[etcd/a $1 NODE_NAME=$NAME" $BASEPATH/hosts

    # check if playbook runs successfully
    ansible-playbook $BASEPATH/tools/19.addetcd.yml -e NODE_TO_ADD=$1 || { sed -i "/$1 NODE_NAME=$NAME/d" $BASEPATH/hosts; return 2; }

    # restart apiservers to use the new etcd cluster
    ansible-playbook $BASEPATH/04.kube-master.yml -t restart_master || { echo "[ERROR] Unexpected failures in master nodes!"; return 2; }

    # save current cluster context if needed
    [ -f "$BASEPATH/.cluster/current_cluster" ] && save_context
    return 0
}

function del-etcd() {
    # check node's address regexp
    [[ $1 =~ ^(2(5[0-5]{1}|[0-4][0-9]{1})|[0-1]?[0-9]{1,2})(\.(2(5[0-5]{1}|[0-4][0-9]{1})|[0-1]?[0-9]{1,2})){3}$ ]] || { echo "[ERROR] Invalid ip address!"; return 2; }

    # 
    ansible-playbook $BASEPATH/tools/remove_etcd_node.yml -e ETCD_TO_DEL=$1

    # restart apiservers to use the new etcd cluster
    ansible-playbook $BASEPATH/04.kube-master.yml -t restart_master || { echo "[ERROR] Unexpected failures in master nodes!"; return 2; }

    # save current cluster context if needed
    [ -f "$BASEPATH/.cluster/current_cluster" ] && save_context
    return 0
}

function clean-node() {
    # check node's address regexp
    [[ $1 =~ ^(2(5[0-5]{1}|[0-4][0-9]{1})|[0-1]?[0-9]{1,2})(\.(2(5[0-5]{1}|[0-4][0-9]{1})|[0-1]?[0-9]{1,2})){3}$ ]] || { echo "[ERROR] Invalid ip address!"; return 2; }

    # 
    ansible-playbook $BASEPATH/tools/clean_one_node.yml -e NODE_TO_DEL=$1

    # save current cluster context if needed
    [ -f "$BASEPATH/.cluster/current_cluster" ] && save_context
    return 0
}

function upgrade() {
    echo -e "[INFO] prepare the new binaries in advance"
    echo -e "[INFO] upgrade begin in 5s, press any key to abort\n:"
    ! (read -t5 -n1 ANS) || { echo "[WARN] upgrade aborted"; return 1; }
    ansible-playbook -t upgrade_k8s $BASEPATH/22.upgrade.yml || return 1
    [ -f "$BASEPATH/.cluster/current_cluster" ] && save_context
    return 0
}
### cluster-wide operation functions ############################

function save_context() {
    [ -f "$BASEPATH/.cluster/current_cluster" ] || { echo "[WARN] Invalid Context"; return 0; }
    CLUSTER=$(cat $BASEPATH/.cluster/current_cluster)
    echo "[INFO] save context: $CLUSTER"
    echo "[INFO] save $CLUSTER roles' configration"
    for ROLE in $(ls $BASEPATH/roles);
    do
        if [ -d "$BASEPATH/roles/$ROLE/defaults" ]; then
            mkdir -p $BASEPATH/.cluster/$CLUSTER/roles/$ROLE/defaults/
            cp -fpr $BASEPATH/roles/$ROLE/defaults/* $BASEPATH/.cluster/$CLUSTER/roles/$ROLE/defaults/
        fi
    done
    
    if [ -f "$BASEPATH/hosts" ];then
        echo "[INFO] save $CLUSTER ansible hosts"
        cp -fp $BASEPATH/hosts $BASEPATH/.cluster/$CLUSTER/
    fi
    
    if [ -f /root/.kube/config ];then
        echo "[INFO] save $CLUSTER kubeconfig"
        cp -fp /root/.kube/config $BASEPATH/.cluster/$CLUSTER/
    fi
}

function install_context() {
    [ -f "$BASEPATH/.cluster/current_cluster" ] || { echo "[ERROR] Invalid Context"; return 1; }
    CLUSTER=$(cat $BASEPATH/.cluster/current_cluster)
    echo "[INFO] install context: $CLUSTER"
    echo "[INFO] install $CLUSTER roles' configration"
    for ROLE in $(ls $BASEPATH/.cluster/$CLUSTER/roles);
    do
        cp -fp $BASEPATH/.cluster/$CLUSTER/roles/$ROLE/defaults/* $BASEPATH/roles/$ROLE/defaults/
    done
    
    if [ -f "$BASEPATH/.cluster/$CLUSTER/hosts" ];then
        echo "[INFO] install $CLUSTER ansible hosts"
        cp -fp $BASEPATH/.cluster/$CLUSTER/hosts $BASEPATH/
    fi
    
    if [ -f "$BASEPATH/.cluster/$CLUSTER/config" ];then
        echo "[INFO] install $CLUSTER kubeconfig"
        cp -fp $BASEPATH/.cluster/$CLUSTER/config /root/.kube/
    fi
}

function checkout() {
    # check directory '.cluster', initialize it if not existed
    if [ ! -d "$BASEPATH/.cluster" ]; then
        echo "[INFO] initialize directory $BASEPATH/.cluster"
        mkdir -p $BASEPATH/.cluster/default
        echo default > $BASEPATH/.cluster/current_cluster
    fi
    # check if $1 is already the current context
    CLUSTER=$(cat $BASEPATH/.cluster/current_cluster)
    [ "$1" != "$CLUSTER" ] || { echo "[WARN] $1 is already the current context"; return 0; }
    # save context of the current cluster
    echo "[INFO] save current context: $CLUSTER"
    save_context
    echo "[INFO] clean context: $CLUSTER"
    rm -rf $BASEPATH/hosts /root/.kube/*
    # check context $1, install it if existed, otherwise initialize it using default context
    if [ ! -d "$BASEPATH/.cluster/$1" ];then
        echo "[INFO] context $1 not existed, initialize it using default context"
        cp -rp $BASEPATH/.cluster/default $BASEPATH/.cluster/$1
        rm -f $BASEPATH/.cluster/$1/hosts $BASEPATH/.cluster/$1/config
    fi
    echo "[INFO] change current context to $1"
    echo $1 > $BASEPATH/.cluster/current_cluster
    install_context;
}

function setup() {
    [ -d "$BASEPATH/.cluster" ] || { echo "[ERROR] invalid context, run 'easzctl checkout <cluster_name>' first"; return 1; }
    [ -f "$BASEPATH/bin/kube-apiserver" ] || { echo "[ERROR] no binaries found, download then fist"; return 1; }
    [ -f "$BASEPATH/hosts" ] || { echo "[ERROR] no ansible hosts found, read 'docs/setup/00-planning_and_overall_intro.md'"; return 1; }
    CLUSTER=$(cat $BASEPATH/.cluster/current_cluster)
    echo -e "\n[INFO] setup cluster with context: $CLUSTER"
    echo -e "[INFO] setup begin in 5s, press any key to abort\n:"
    ! (read -t5 -n1 ANS) || { echo "[WARN] setup aborted"; return 1; }
    ansible-playbook $BASEPATH/90.setup.yml || return 1
    save_context
}

function list() {
    [ -d "$BASEPATH/.cluster" ] || { echo "[ERROR] invalid context, run 'easzctl checkout <cluster_name>' first"; return 1; }
    CLUSTER=$(cat $BASEPATH/.cluster/current_cluster)
    echo -e "\nlist of managed contexts (current: $CLUSTER)"
    i=1; for Cluster in $(ls $BASEPATH/.cluster/ |grep -v current_cluster);
    do
        echo -e "==> context $i:\t$Cluster"
        let "i++"
    done
    echo -e "\nlist of installed clusters (current: $CLUSTER)"
    i=1; for Cluster in $(ls $BASEPATH/.cluster/ |grep -v current_cluster);
    do
        KUBECONF=$BASEPATH/.cluster/$Cluster/config
        if [ -f "$KUBECONF" ]; then
            echo -e "==> cluster $i:\t$Cluster"
            $BASEPATH/bin/kubectl --kubeconfig=$KUBECONF get node
        fi
        let "i++"
    done
}

function destroy() {
    [ -d "$BASEPATH/.cluster" ] || { echo "[ERROR] invalid context, run 'easzctl checkout <cluster_name>' first"; return 1; }
    CLUSTER=$(cat $BASEPATH/.cluster/current_cluster)
    echo -n "[WARN] DELETE cluster: $CLUSTER, Continue? (y/n): "
    read -t10 -n1 ANS || { echo -e "\n[WARN] timeout, destroy aborted"; return 1; }
    if [[ -n $ANS && $ANS == y ]];then
        echo -e "\n[INFO] clean all nodes of cluster in 5s"
        sleep 5
        ansible-playbook $BASEPATH/99.clean.yml
        rm -f $BASEPATH/.cluster/$CLUSTER/config
        [ "$#" -gt 0 ] || { return 0; }
        if [[ -n $1 && $1 == --purge ]];then
            echo "[INFO] delete current context"
            rm -rf $BASEPATH/.cluster/$CLUSTER
            rm -rf $BASEPATH/hosts /root/.kube/*
            echo "[INFO] change current context to default"
            echo default > $BASEPATH/.cluster/current_cluster
            install_context
        fi
    else
        echo -e "\n[WARN] destroy aborted"; return 1;
    fi
}

function start-aio(){
    checkout aio
    set +u
    # Check ENV 'HOST_IP', if exist indecates running in a docker container, otherwise running in a host machine
    if [[ -z $HOST_IP ]];then
        # easzctl runs in a host machine, get host's ip
        HOST_IF=$(ip route|grep default|cut -d' ' -f5)
        HOST_IP=$(ip a|grep $HOST_IF|awk 'NR==2{print $2}'|cut -d'/' -f1)
    fi
    set -u
    cp -f $BASEPATH/example/hosts.allinone.example.en $BASEPATH/hosts
    sed -i "s/192.168.1.1/$HOST_IP/g" $BASEPATH/hosts
    setup
}

### extra operation functions ###################################

function basic-auth(){
    OPTIND=2
    CONFIG=$BASEPATH/roles/kube-master/defaults/main.yml
    EX_VARS=""
    while getopts "sSu:p:" OPTION; do
        case $OPTION in
          s)
            EX_VARS="BASIC_AUTH_ENABLE=yes $EX_VARS"
            ENABLED=yes
            ;;
          S)
            grep BASIC_AUTH_ENABLE $CONFIG|grep no > /dev/null && \
            { echo -e "\n[WARN]basic-auth already disabled!\n"; return 1; }
            EX_VARS="BASIC_AUTH_ENABLE=no $EX_VARS"
            ENABLED=no
            ;;
          u)
            EX_VARS="BASIC_AUTH_USER=$OPTARG $EX_VARS"
            sed -i "s/BASIC_AUTH_USER.*$/BASIC_AUTH_USER: '$OPTARG'/g" $CONFIG
            ;;
          p)
            EX_VARS="BASIC_AUTH_PASS=$OPTARG $EX_VARS"
            sed -i "s/BASIC_AUTH_PASS.*$/BASIC_AUTH_PASS: '$OPTARG'/g" $CONFIG
            ;;
          ?)
            help-info basic-auth
            return 1
            ;;
        esac
    done
    
    ansible-playbook $BASEPATH/04.kube-master.yml -t restart_master -e "$EX_VARS" || { return 1; } 
    sed -i "s/BASIC_AUTH_ENABLE.*$/BASIC_AUTH_ENABLE: '$ENABLED'/g" $CONFIG 
    if [[ $ENABLED == yes ]];then
        echo -e "\n[INFO]basic-auth for apiserver is enabled!"
        sed -n '/BASIC_AUTH_USER/p' $CONFIG
        sed -n '/BASIC_AUTH_PASS/p' $CONFIG
    elif [[ $ENABLED == no ]];then
        echo -e "\n[INFO]basic-auth for apiserver is disabled!\n"
    fi
    # save current cluster context if needed
    [ -f "$BASEPATH/.cluster/current_cluster" ] && save_context
    return 0
}

### Main Lines ##################################################

BASEPATH=/etc/ansible

[ "$#" -gt 0 ] || { usage >&2; exit 2; }

case "$1" in
    ### in-cluster operations #####################
    (add-node)
        [ "$#" -gt 1 ] || { usage >&2; exit 2; }
        ACTION="Action: add a k8s work node"
        CMD="add-node $2" 
        ;;
    (add-master)
        [ "$#" -gt 1 ] || { usage >&2; exit 2; }
        ACTION="Action: add a k8s master node"
        CMD="add-master $2" 
        ;;
    (add-etcd)
        [ "$#" -gt 1 ] || { usage >&2; exit 2; }
        ACTION="Action: add a etcd node"
        CMD="add-etcd $2" 
        ;;
    (del-etcd)
        [ "$#" -gt 1 ] || { usage >&2; exit 2; }
        ACTION="Action: delete a etcd node"
        CMD="del-etcd $2" 
        ;;
    (clean-node)
        [ "$#" -gt 1 ] || { usage >&2; exit 2; }
        ACTION="Action: clean a node"
        CMD="clean-node $2" 
        ;;
    (upgrade)
        ACTION="Action: upgrade the cluster"
        CMD="upgrade" 
        ;;
    ### cluster-wide operations #######################
    (checkout)
        [ "$#" -gt 1 ] || { usage >&2; exit 2; }
        ACTION="Action: checkout cluster context"
        CMD="checkout $2" 
        ;;
    (destroy)
        ACTION="Action: destroy current cluster"
        if [ "$#" -gt 1 ];then
            CMD="destroy $2"
        else
            CMD="destroy"
        fi
        ;;
    (list)
        ACTION="Action: list all of clusters managed"
        CMD="list" 
        ;;
    (setup)
        ACTION="Action: setup cluster with current context"
        CMD="setup" 
        ;;
    (start-aio)
        ACTION="Action: start an AllInOne cluster"
        CMD="start-aio" 
        ;;
    (help)
        [ "$#" -gt 1 ] || { usage >&2; exit 2; }
        help-info $2
        exit 0
        ;;
    ### extra operations ##############################
    (basic-auth)
        [ "$#" -gt 1 ] || { help-info $1; exit 2; }
        ACTION="Action: enable/disable apiserver's basic-auth"
        CMD="basic-auth $*"
        ;;
    (*)
        usage
        exit 0
        ;;
esac

process_cmd

